use log::{debug, error, warn};
use smithay_client_toolkit::{
    reexports::{
        client::{
            Connection, Proxy as _, QueueHandle,
            protocol::{wl_display::WlDisplay, wl_seat::WlSeat, wl_surface::WlSurface},
        },
        protocols::wp::viewporter::client::wp_viewport::WpViewport,
    },
    seat::pointer::{CursorIcon, ThemedPointer},
    shell::{
        WaylandSurface,
        xdg::{
            XdgSurface,
            window::{DecorationMode, Window, WindowConfigure, WindowDecorations},
        },
    },
    shm::Shm,
};

use crate::linux::{
    application_api::RenderingMode,
    application_state::{ApplicationState, EglInstance},
    events::{SoftwareDrawData, WindowDecorationMode, WindowDrawEvent, WindowId},
    geometry::{LogicalPixels, LogicalPoint, LogicalSize, PhysicalSize},
    pointer_shapes_api::PointerShape,
    rendering_egl::EglRendering,
    rendering_software::SoftwareRendering,
    window_api::WindowParams,
    window_resize_edge_api::WindowResizeEdge,
};

#[derive(Debug)]
pub enum RenderingData {
    Egl(EglRendering<'static>),
    Software(SoftwareRendering),
}

impl RenderingData {
    pub fn draw<F>(&mut self, surface: &WlSurface, size: PhysicalSize, do_draw: F)
    where
        F: FnOnce(SoftwareDrawData) -> bool,
    {
        match self {
            Self::Egl(r) => r.draw(surface, do_draw),
            Self::Software(r) => r.draw(surface, size, do_draw),
        }
    }
}

impl From<DecorationMode> for WindowDecorationMode {
    fn from(value: DecorationMode) -> Self {
        match value {
            DecorationMode::Client => Self::Client,
            DecorationMode::Server => Self::Server,
        }
    }
}

pub struct SimpleWindow {
    pub window_id: WindowId,
    pub app_id: String,
    pub close: bool,
    pub size: Option<LogicalSize>,
    viewport: Option<WpViewport>,
    pub window: Window,
    pub set_cursor: bool,
    decorations_cursor: Option<CursorIcon>,
    pub current_scale: f64,
    decoration_mode: DecorationMode,
    rendering_data: Option<RenderingData>,
    pub rendering_mode: RenderingMode,
    pub num_pointer_buttons_down: u32,
}

impl SimpleWindow {
    #[must_use]
    pub fn new(window_id: WindowId, app_state: &ApplicationState, qh: &QueueHandle<ApplicationState>, params: &WindowParams) -> Self {
        let state = app_state;

        let window_surface = state.compositor_state.create_surface(qh);

        let surface_id = window_surface.id();

        if let Some(fractional_scale_manager) = state.fractional_scale_manager.as_ref() {
            fractional_scale_manager.get_fractional_scale(&window_surface, qh, surface_id.clone());
        }

        let viewport = state.viewporter.as_ref().map(|vp| vp.get_viewport(&window_surface, qh, ()));

        let decorations = if params.prefer_client_side_decoration {
            WindowDecorations::RequestClient
        } else {
            WindowDecorations::ServerDefault
        };
        let window = state.xdg_shell_state.create_window(window_surface, decorations, qh);
        let app_id = params.app_id.as_str().unwrap().to_owned();
        window.set_title(params.title.as_str().unwrap());
        window.set_app_id(app_id.clone());

        let size = if params.size.width.0 == 0.0 { None } else { Some(params.size) };

        // In order for the window to be mapped, we need to perform an initial commit with no attached buffer.
        // For more info, see WaylandSurface::commit
        //
        // The compositor will respond with an initial configure that we can then use to present to the window with
        // the correct options.
        window.commit();

        debug!("Creating new window with id={:?} and surface_id={surface_id}", params.window_id);
        Self {
            window_id,
            app_id,
            close: false,
            size,
            viewport,
            window,
            set_cursor: false,
            decorations_cursor: Some(CursorIcon::Default),
            current_scale: 1.0,
            decoration_mode: DecorationMode::Client,
            rendering_data: None,
            rendering_mode: params.rendering_mode,
            num_pointer_buttons_down: 0,
        }
    }

    pub const fn close(&mut self) {
        self.close = true;
    }

    pub fn configure(
        &mut self,
        wl_display: &WlDisplay,
        shm: &Shm,
        window: &Window,
        configure: &WindowConfigure,
        egl: Option<&'static EglInstance>,
    ) -> bool {
        const DEFAULT_WIDTH: LogicalPixels = LogicalPixels(640.);
        const DEFAULT_HEIGHT: LogicalPixels = LogicalPixels(480.);
        debug!("SimpleWindow::configure start: {configure:?}");

        self.decoration_mode = configure.decoration_mode;

        let width = configure
            .new_size
            .0
            .map(|w| LogicalPixels(w.get().into()))
            .or_else(|| self.size.map(|s| s.width))
            .or_else(|| configure.suggested_bounds.map(|(w, _h)| LogicalPixels(w.into())))
            .unwrap_or(DEFAULT_WIDTH);
        let height = configure
            .new_size
            .1
            .map(|h| LogicalPixels(h.get().into()))
            .or_else(|| self.size.map(|s| s.height))
            .or_else(|| configure.suggested_bounds.map(|(_w, h)| LogicalPixels(h.into())))
            .unwrap_or(DEFAULT_HEIGHT);
        let size = LogicalSize { width, height };
        self.size = Some(size);

        window.xdg_surface().set_window_geometry(0, 0, width.round(), height.round());
        // TODO: wl_surface::set_opaque_region?

        let physical_size = size.to_physical(self.current_scale);
        debug!("SimpleWindow::configure: size={size:?}, physical_size={physical_size:?}");

        self.on_resize(&size, physical_size, shm);

        let is_first_configure = self.rendering_data.is_none();
        if is_first_configure {
            self.rendering_data = if let Some(egl) = egl {
                match EglRendering::new(egl, wl_display, window.wl_surface(), physical_size) {
                    Ok(egl_rendering_data) => Some(RenderingData::Egl(egl_rendering_data)),
                    Err(e) => {
                        warn!("Failed to create EGL rendering, falling back to software rendering. Error: {e:?}");
                        Some(RenderingData::Software(SoftwareRendering::new(shm, physical_size)))
                    }
                }
            } else {
                Some(RenderingData::Software(SoftwareRendering::new(shm, physical_size)))
            };
        }
        is_first_configure
    }

    pub fn draw(
        &mut self,
        conn: &Connection,
        qh: &QueueHandle<ApplicationState>,
        themed_pointer: Option<&mut ThemedPointer>,
        callback: &dyn Fn(WindowDrawEvent) -> bool,
    ) {
        let surface = self.window.wl_surface();
        if self.set_cursor
            && let Some(themed_pointer) = themed_pointer
        {
            debug!("Updating cursor to {:?} for {}", self.decorations_cursor, surface.id());
            if let Some(decorations_cursor) = self.decorations_cursor {
                match themed_pointer.set_cursor(conn, decorations_cursor) {
                    Ok(()) => {
                        self.set_cursor = false;
                    }
                    Err(e) => {
                        error!("Failed to set cursor, error: {e:?}");
                    }
                }
            } else if let Err(e) = themed_pointer.hide_cursor() {
                warn!("Failed to hide cursor: {e}");
            } else {
                self.set_cursor = false;
            }
        }

        let physical_size = self.size.unwrap().to_physical(self.current_scale);

        let do_draw = |software_draw_data: SoftwareDrawData| {
            let did_draw = callback(WindowDrawEvent {
                window_id: self.window_id,
                software_draw_data,
                physical_size,
                scale: self.current_scale,
            });

            if did_draw {
                // Damage the entire window
                surface.damage_buffer(0, 0, physical_size.width.0, physical_size.height.0);
            }

            // Request our next frame
            surface.frame(qh, surface.clone());
            did_draw
        };

        if let Some(r) = &mut self.rendering_data {
            r.draw(surface, physical_size, do_draw);
        } else {
            warn!("Rendering data not initialized in draw");
        }

        surface.commit();
    }

    fn on_resize(&mut self, size: &LogicalSize, physical_size: PhysicalSize, shm: &Shm) {
        if let Some(viewport) = &self.viewport {
            debug!("viewport.set_destination({}, {})", size.width.round(), size.height.round());
            viewport.set_destination(size.width.round(), size.height.round());
        } else {
            let surface = self.window.wl_surface();
            assert!(self.current_scale % 1.0 < 0.0001);
            debug!("surface.set_buffer_scale({})", self.current_scale);
            #[allow(clippy::cast_possible_truncation)]
            surface.set_buffer_scale(self.current_scale as i32);
        }

        if let Some(rendering_data) = &mut self.rendering_data {
            match rendering_data {
                RenderingData::Egl(egl_data) => {
                    egl_data.resize(physical_size);
                }
                RenderingData::Software(data) => {
                    data.resize(shm, physical_size);
                }
            }
        }
    }

    pub fn scale_changed(&mut self, new_scale: f64, shm: &Shm) {
        debug!("scale_changed: {new_scale}");
        self.current_scale = new_scale;

        if let Some(size) = self.size {
            self.on_resize(&size, size.to_physical(self.current_scale), shm);
        }
    }

    pub fn set_cursor_icon(&mut self, pointer_shape: PointerShape) {
        let cursor_icon = pointer_shape.into();
        if self.decorations_cursor != cursor_icon {
            self.set_cursor = true;
            self.decorations_cursor = cursor_icon;
        }
    }

    pub fn start_move(&self, seat: &WlSeat, serial: u32) {
        self.window.move_(seat, serial);
    }

    pub fn start_resize(&self, edge: WindowResizeEdge, seat: &WlSeat, serial: u32) {
        self.window.resize(seat, serial, edge.into());
    }

    pub fn show_menu(&self, position: LogicalPoint, seat: &WlSeat, serial: u32) {
        self.window.show_window_menu(seat, serial, (position.x.round(), position.y.round()));
    }
}
